package com.googlecode.compliancechecker;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;

/**
 * Checks the compiled classes whether they are compliant to the configured
 * android API level.
 * 
 * @author Marc-Christian Schulze
 * @goal check
 * @phase process-classes
 */
public class CheckComplianceMojo extends AbstractMojo {

	private static final String	PKG_LIST_MISSING;
	private static final String	COMPLIANCE_MISMATCH;
	private static final String	NO_ERRORS;
	private static final String	SKIP_NON_CLASS;
	private static final String	IS_COMPLIANT;
	private static final String	NOT_COMPLIANT;
	private static final String	CANNOT_CHECK;
	static {
		PKG_LIST_MISSING = "Failed to load the jdk package list file.";
		COMPLIANCE_MISMATCH = "There were compliance mismatches in the class files.";
		NO_ERRORS = "No compliance errors detected.";
		SKIP_NON_CLASS = "Skipping non-class file %1$s";
		IS_COMPLIANT = "Class[%1$s] with apiLevel %2$d is compliant.";
		NOT_COMPLIANT = "Class[%1$s] is not compliant to apiLevel %2$d";
		CANNOT_CHECK = "Can't check compliance to apiLevel %1$d. Reason: %2$s";
	}

	/**
	 * The android API level.
	 * 
	 * @parameter
	 */
	private int					defaultApiLevel	= 7;

	/**
	 * The directory to scan for class files that should be checked.
	 * 
	 * @parameter expression="${project.build.outputDirectory}"
	 */
	private File				targetDirectory;

	/**
	 * The version of the used jdk. Currently supported are 1.4, 1.5, 1.6 and
	 * 1.7
	 * 
	 * @parameter expression="${jdk.version}"
	 */
	private String				jdkVersion		= "1.6";

	/**
	 * @parameter expression="${env.ANDROID_HOME}"
	 */
	private File				androidHome;

	/**
	 * The name of the attribute of the annotation that specifies the android
	 * api level.
	 * 
	 * @parameter
	 */
	private final String		attributeName	= "apiLevel";

	/**
	 * The name of the annotation that should be used to enable the compliance
	 * check. You can either specify a full qualified annotation name or just
	 * the name.
	 * 
	 * @parameter
	 */
	private String				annotationName	= "AndroidCompatible";

	private List<String>		packagesToScan;
	private boolean				wasComplianceCheckSuccessful;

	public void execute() throws MojoExecutionException {
		printConfiguration();

		try {
			packagesToScan = loadScanPackages();
		} catch (IOException e) {
			throw new MojoExecutionException(PKG_LIST_MISSING, e);
		}

		wasComplianceCheckSuccessful = true;
		processDirectoryRecursive(targetDirectory);
		if (!wasComplianceCheckSuccessful) {
			throw new MojoExecutionException(COMPLIANCE_MISMATCH);
		}
		getLog().info(NO_ERRORS);
	}

	private void printConfiguration() {
		getLog().debug("Configuration:");
		getLog().debug("defaultApiLevel: " + defaultApiLevel);
		getLog().debug("targetDirectory: " + targetDirectory);
		getLog().debug("jdkVersion: " + jdkVersion);
		getLog().debug("androidHome: " + androidHome);
		getLog().debug("attributeName: " + attributeName);
		getLog().debug("annotationName: " + annotationName);
	}

	private void processDirectoryRecursive(File directory) {
		for (File file : directory.listFiles()) {
			if (file.isDirectory()) {
				processDirectoryRecursive(file);
			} else {
				processFile(file);
			}
		}
	}

	private void processFile(File file) {
		if (file.getName().endsWith(".class")) {
			checkClassFile(file);
		} else {
			getLog().debug(String.format(SKIP_NON_CLASS, file.toString()));
		}
	}

	private void checkClassFile(File classFile) {
		try {
			tryToCheckFile(classFile);
		} catch (FileNotFoundException e) {
			getLog().error(e);
		} catch (IOException e) {
			getLog().error(e);
		}
	}

	private void tryToCheckFile(File classFile) throws IOException {
		ComplianceLevelDetector detector;
		detector = new ComplianceLevelDetector(annotationName, attributeName);
		FileInputStream stream = new FileInputStream(classFile);
		int apiLevel;
		try {
			apiLevel = detector.getApiLevel(stream);
		} finally {
			if (stream != null) {
				stream.close();
			}
		}
		if (apiLevel > 0) {
			verifyFile(classFile, apiLevel);
		}
	}

	private void verifyFile(File classFile, int apiLevel) throws IOException {
		try {
			ComplianceChecker checker = new ComplianceChecker(androidHome, apiLevel, packagesToScan);
			boolean compliant = checker.isCompliant(new FileInputStream(classFile));
			if (compliant) {
				getLog().debug(String.format(IS_COMPLIANT, classFile, apiLevel));
			} else {
				getLog().error(String.format(NOT_COMPLIANT, classFile, apiLevel));

				List<TypeReference> missingReferences;
				missingReferences = checker.getMissingReferences();
				for (TypeReference ref : missingReferences) {
					getLog().error("Missing " + ref);
				}
				wasComplianceCheckSuccessful = false;
			}
		} catch (FileNotFoundException e) {
			getLog().error(String.format(CANNOT_CHECK, apiLevel, e.getMessage()));
			wasComplianceCheckSuccessful = false;
		}
	}

	private List<String> loadScanPackages() throws IOException {
		List<String> packages = new ArrayList<String>();

		String pkgListFile = "jdk" + jdkVersion + "Packages.txt";
		InputStream stream = getClass().getResourceAsStream(pkgListFile);
		BufferedReader reader = null;
		try {
			reader = new BufferedReader(new InputStreamReader(stream));
			String line;
			while ((line = reader.readLine()) != null) {
				if (line.trim().length() == 0) {
					continue;
				}
				packages.add(line.trim());
			}
		} finally {
			if (stream != null) {
				stream.close();
			}
		}
		return packages;
	}
}